PEAK_ASYM
Overview
The PEAK_ASYM function fits a variety of asymmetric and symmetric peak models to x-y data using nonlinear least squares optimization. Peak fitting is essential in spectroscopy, chromatography, signal processing, and any domain where data exhibits bell-shaped or pulse-like patterns that need to be characterized mathematically.
This implementation uses scipy.optimize.curve_fit, which applies the Levenberg-Marquardt algorithm (or trust region methods when bounds are specified) to minimize the sum of squared residuals between the model and the observed data. The function returns both the fitted parameter values and their standard errors, derived from the covariance matrix of the fit.
The function supports nine peak models:
Gaussian Peak variants (Area, Amplitude, and FWHM parameterizations): The classic symmetric bell curve described by f(x) = y_0 + A \exp\left(-\frac{(x-x_c)^2}{2w^2}\right). The Gaussian function appears throughout statistics, physics, and signal processing due to the central limit theorem.
Asymmetric Double Sigmoidal Peak: Combines two logistic (sigmoidal) functions to create an asymmetric peak with independent rise and fall characteristics, useful for peaks with different leading and trailing edge behaviors.
Logistic Peak (Hubbert Curve): Based on the logistic distribution, this model produces a symmetric peak with heavier tails than a Gaussian. Originally developed by M. King Hubbert for modeling resource depletion, it is defined as f(x) = y_0 + \frac{4A e^{-(x-x_c)/w}}{(1 + e^{-(x-x_c)/w})^2}.
Power Symmetric/Asymmetric: Power-law models where intensity scales as |x - x_c|^P, with variants supporting offsets, centered forms, and asymmetric dual exponents for different behaviors on each side of the center.
Exponential Pulse (Rise-Decay): Models pulses with an exponential rise followed by exponential decay, commonly used for transient signals in electronics and photophysics.
For each model, initial parameter guesses are computed automatically from the data characteristics (range, median, amplitude), and appropriate parameter bounds ensure physically meaningful results. For more information, see the SciPy curve_fit documentation and the SciPy GitHub repository.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=PEAK_ASYM(xdata, ydata, peak_asym_model)
xdata(list[list], required): The xdata valueydata(list[list], required): The ydata valuepeak_asym_model(str, required): The peak_asym_model value
Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.
Examples
Example 1: Demo case 1
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| asymmetric_double_sigmoidal_peak | 0.01 | 0.6006047045310022 |
| 2.0075 | 1.0737954138646513 | |
| 4.005 | 0.43007294650211997 | |
| 6.0024999999999995 | 0.06091388592739846 | |
| 8 | 0.019487637769087587 |
Excel formula:
=PEAK_ASYM("asymmetric_double_sigmoidal_peak", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.6006047045310022;1.0737954138646513;0.43007294650211997;0.06091388592739846;0.019487637769087587})
Expected output:
"non-error"
Example 2: Demo case 2
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| gaussian_peak_area_parameterized | 0.01 | 0.03025502969436638 |
| 2.0075 | 1.898441420966237 | |
| 4.005 | 0.03062760430597533 | |
| 6.0024999999999995 | 0.01 | |
| 8 | 0.015251619698276176 |
Excel formula:
=PEAK_ASYM("gaussian_peak_area_parameterized", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.03025502969436638;1.898441420966237;0.03062760430597533;0.01;0.015251619698276176})
Expected output:
"non-error"
Example 3: Demo case 3
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| gaussian_peak_amplitude_parameterized | 0.01 | 0.7259943748121195 |
| 2.0075 | 2.7302902790530585 | |
| 4.005 | 0.31525061168113216 | |
| 6.0024999999999995 | 0.01 | |
| 8 | 0.02066777122773472 |
Excel formula:
=PEAK_ASYM("gaussian_peak_amplitude_parameterized", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.7259943748121195;2.7302902790530585;0.31525061168113216;0.01;0.02066777122773472})
Expected output:
"non-error"
Example 4: Demo case 4
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| gaussian_peak_fwhm_parameterized | 0.01 | 0.025571345980890503 |
| 2.0075 | 2.1338126889403566 | |
| 4.005 | 0.034234664064067055 | |
| 6.0024999999999995 | 0.01 | |
| 8 | 0.01715983261300243 |
Excel formula:
=PEAK_ASYM("gaussian_peak_fwhm_parameterized", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.025571345980890503;2.1338126889403566;0.034234664064067055;0.01;0.01715983261300243})
Expected output:
"non-error"
Example 5: Demo case 5
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| logistic_peak_hubbert_curve | 0.01 | 1.5079112140631585 |
| 2.0075 | 2.7688893016375915 | |
| 4.005 | 1.0697838701118711 | |
| 6.0024999999999995 | 0.14630301232765053 | |
| 8 | 0.04847030906827912 |
Excel formula:
=PEAK_ASYM("logistic_peak_hubbert_curve", {0.01;2.0075;4.005;6.0024999999999995;8}, {1.5079112140631585;2.7688893016375915;1.0697838701118711;0.14630301232765053;0.04847030906827912})
Expected output:
"non-error"
Example 6: Demo case 6
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| power_symmetric_with_offset | 0.1 | 46.51368448781824 |
| 1.3250000000000002 | 25.35817972099038 | |
| 2.5500000000000003 | 17.794906308746665 | |
| 3.7750000000000004 | 80.56414349732965 | |
| 5 | 1066.128702416852 |
Excel formula:
=PEAK_ASYM("power_symmetric_with_offset", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {46.51368448781824;25.35817972099038;17.794906308746665;80.56414349732965;1066.128702416852})
Expected output:
"non-error"
Example 7: Demo case 7
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| power_symmetric_centered | 0.1 | 46.51368448781824 |
| 1.3250000000000002 | 25.35817972099038 | |
| 2.5500000000000003 | 17.794906308746665 | |
| 3.7750000000000004 | 80.56414349732965 | |
| 5 | 1066.128702416852 |
Excel formula:
=PEAK_ASYM("power_symmetric_centered", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {46.51368448781824;25.35817972099038;17.794906308746665;80.56414349732965;1066.128702416852})
Expected output:
"non-error"
Example 8: Demo case 8
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| power_asymmetric_dual_exponent | 0.1 | 46.51368448781824 |
| 1.3250000000000002 | 25.35817972099038 | |
| 2.5500000000000003 | 17.794906308746665 | |
| 3.7750000000000004 | 80.56414349732965 | |
| 5 | 1066.128702416852 |
Excel formula:
=PEAK_ASYM("power_asymmetric_dual_exponent", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {46.51368448781824;25.35817972099038;17.794906308746665;80.56414349732965;1066.128702416852})
Expected output:
"non-error"
Example 9: Demo case 9
Inputs:
| peak_asym_model | xdata | ydata |
|---|---|---|
| exp_pulse_rise_decay | 0.01 | 0.01 |
| 2.0075 | 0.01 | |
| 4.005 | 0.06168147075764583 | |
| 6.0024999999999995 | 0.13906182126049085 | |
| 8 | 0.17613759877451055 |
Excel formula:
=PEAK_ASYM("exp_pulse_rise_decay", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.01;0.01;0.06168147075764583;0.13906182126049085;0.17613759877451055})
Expected output:
"non-error"
Python Code
import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math
def peak_asym(xdata, ydata, peak_asym_model):
"""
Fits peak_asym models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.
See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
This example function is provided as-is without any representation of accuracy.
Args:
xdata (list[list]): The xdata value
ydata (list[list]): The ydata value
peak_asym_model (str): The peak_asym_model value Valid options: Asymmetric Double Sigmoidal Peak, Gaussian Peak Area Parameterized, Gaussian Peak Amplitude Parameterized, Gaussian Peak Fwhm Parameterized, Logistic Peak Hubbert Curve, Power Symmetric With Offset, Power Symmetric Centered, Power Asymmetric Dual Exponent, Exp Pulse Rise Decay.
Returns:
list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
"""
def _validate_data(xdata, ydata):
"""Validate and convert both xdata and ydata to numpy arrays."""
for name, arg in [("xdata", xdata), ("ydata", ydata)]:
if not isinstance(arg, list) or len(arg) < 2:
raise ValueError(f"{name}: must be a 2D list with at least two rows")
vals = []
for i, row in enumerate(arg):
if not isinstance(row, list) or len(row) == 0:
raise ValueError(f"{name} row {i}: must be a non-empty list")
try:
vals.append(float(row[0]))
except Exception:
raise ValueError(f"{name} row {i}: non-numeric value")
if name == "xdata":
x_arr = np.asarray(vals, dtype=np.float64)
else:
y_arr = np.asarray(vals, dtype=np.float64)
if x_arr.shape[0] != y_arr.shape[0]:
raise ValueError("xdata and ydata must have the same number of rows")
return x_arr, y_arr
# Model definitions dictionary
models = {
'asymmetric_double_sigmoidal_peak': {
'params': ['y0', 'xc', 'A', 'w1', 'w2', 'w3'],
'model': lambda x, y0, xc, A, w1, w2, w3: y0 + A / (1.0 + np.exp(-(x - xc + 0.5 * w1) / w2)) * (1.0 - 1.0 / (1.0 + np.exp(-(x - xc - 0.5 * w1) / w3))),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3)), float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3))),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, 0.0, 0.0], np.inf),
},
'gaussian_peak_area_parameterized': {
'params': ['y0', 'xc', 'w', 'A'],
'model': lambda x, y0, xc, w, A: y0 + (A / (w * np.sqrt(np.pi / 2.0))) * np.exp(-2.0 * np.power(x - xc, 2) / np.power(w, 2)),
'guess': lambda xa, ya: (float(min(ya)), float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), float(max(ya))),
'bounds': ([-np.inf, -np.inf, 0.0, -np.inf], np.inf),
},
'gaussian_peak_amplitude_parameterized': {
'params': ['y0', 'xc', 'w', 'A'],
'model': lambda x, y0, xc, w, A: y0 + A * np.exp(-np.power(x - xc, 2) / (2.0 * w * w)),
'guess': lambda xa, ya: (float(min(ya)), float(np.median(xa)), 1.0, float(max(ya) - min(ya))),
'bounds': ([-np.inf, -np.inf, 0.0, -np.inf], np.inf),
},
'gaussian_peak_fwhm_parameterized': {
'params': ['y0', 'xc', 'A', 'w'],
'model': lambda x, y0, xc, A, w: y0 + (A / (w * np.sqrt(np.pi / (4.0 * np.log(2.0))))) * np.exp(-4.0 * np.log(2.0) * np.square(x - xc) / np.square(w)),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3))),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0], np.inf),
},
'logistic_peak_hubbert_curve': {
'params': ['y0', 'xc', 'w', 'A'],
'model': lambda x, y0, xc, w, A: y0 + 4.0 * A * np.exp(-(x - xc) / w) / np.power(1.0 + np.exp(-(x - xc) / w), 2),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 6.0, 1e-3)), float(np.ptp(ya) if np.ptp(ya) else 1.0)),
'bounds': ([-np.inf, -np.inf, 0.0, -np.inf], np.inf),
},
'power_symmetric_with_offset': {
'params': ['y0', 'xc', 'A', 'P'],
'model': lambda x, y0, xc, A, P: y0 + A * np.power(np.abs(x - xc), P),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
'bounds': ([-np.inf, -np.inf, 0.0, 0.0], np.inf),
},
'power_symmetric_centered': {
'params': ['xc', 'A', 'P'],
'model': lambda x, xc, A, P: A * np.power(np.abs(x - xc), P),
'guess': lambda xa, ya: (float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
'bounds': ([-np.inf, 0.0, 0.0], np.inf),
},
'power_asymmetric_dual_exponent': {
'params': ['xc', 'A', 'pl', 'pu'],
'model': lambda x, xc, A, pl, pu: np.where(x < xc, A * np.power(np.abs(x - xc), pl), A * np.power(np.abs(x - xc), pu)),
'guess': lambda xa, ya: (float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0, 1.5),
'bounds': ([-np.inf, 0.0, 0.0, 0.0], np.inf),
},
'exp_pulse_rise_decay': {
'params': ['y0', 'x0', 'A', 't1', 'P', 't2'],
'model': lambda x, y0, x0, A, t1, P, t2: np.where(x >= x0, y0 + A * np.power(1.0 - np.exp(-(x - x0) / t1), P) * np.exp(-(x - x0) / t2), y0),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3)), 2.0, float(max((np.max(xa) - np.min(xa)) / 10.0, 1e-3))),
'bounds': ([-np.inf, -np.inf, 0.0, 0.0, 0.0, 0.0], np.inf),
}
}
# Validate model parameter
if peak_asym_model not in models:
return f"Invalid model: {str(peak_asym_model)}. Valid models are: {', '.join(models.keys())}"
model_info = models[peak_asym_model]
# Validate and convert input data
try:
x_arr, y_arr = _validate_data(xdata, ydata)
except ValueError as e:
return f"Invalid input: {e}"
# Perform curve fitting
try:
p0 = model_info['guess'](x_arr, y_arr)
bounds = model_info.get('bounds', (-np.inf, np.inf))
if bounds == (-np.inf, np.inf):
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
else:
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)
fitted_vals = [float(v) for v in popt]
for v in fitted_vals:
if math.isnan(v) or math.isinf(v):
return "Fitting produced invalid numeric values (NaN or inf)."
except ValueError as e:
return f"Initial guess error: {e}"
except Exception as e:
return f"curve_fit error: {e}"
# Calculate standard errors
std_errors = None
try:
if pcov is not None and np.isfinite(pcov).all():
std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
except Exception:
pass
return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]